﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Basic class to divide the terrain and generate the voxel mesh
/// </summary>
public class Chunk
{
    #region HelperProperties
    public Point3 worldPosition { get { return new Point3(worldX, worldY, worldZ); } }
    public Point3 chunkPosition { get { return new Point3(worldX >> 5, worldY >> 5, worldZ >> 5); } }
    public Point3 regionPosition { get { return new Point3(worldX >> 9, worldY >> 9, worldZ >> 9); } }
    #endregion

    private static Vector3 Offset = new Vector3(-0.5f, 0.5f, -0.5f);

    /// <summary>
    /// The voxel terrain the chunk is placed in
    /// </summary>
    public VoxelTerrain voxelTerrain;
    // Helper fields
    /// <summary>
    /// Unique chunk ID inside the voxel terrain
    /// </summary>
    public static int ID = 0;
    /// <summary>
    /// The unique ID of the chunk
    /// </summary>
    public int m_ID { get; private set; }

    /// <summary>
    /// The x coordinate of the chunk in world coordinates
    /// </summary>
    public int worldX { get; private set; }
    /// <summary>
    /// The x coordinate of the chunk in world coordinates
    /// </summary>
    public int worldY { get; private set; }
    /// <summary>
    /// The x coordinate of the chunk in world coordinates
    /// </summary>
    public int worldZ { get; private set; }

    /// <summary>
    /// BlockIDs defining the terrain, 8 bits per block
    /// </summary>
    public byte[] chunkData { get; private set; }

    /// <summary>
    /// True if chunk is not rendered
    /// </summary>
    public bool IsClean;
    /// <summary>
    /// True if chunk has been calculated
    /// </summary>
    public bool IsBuilded;
    /// <summary>
    /// True if chunk need to be repainted (i.E after change)
    /// </summary>
    public bool NeedRepaint { get; private set; }

    #region Mesh Fields
    /// <summary>
    /// The gameObject on which the mesh is generated

    /// </summary>
    private GameObject gameObject;

    private List<Vector3> Vertices;
    private List<int> Triangles;
    private List<Vector2> UV;

    private Mesh mesh;
    private MeshCollider meshCollider;

    private int faceCount;
    #endregion

    /// <summary>
    /// Contructs a new default Chunk
    /// </summary>
    /// <param name="parent">The voxel terrain the chunk is located in</param>
    /// <param name="PosX">The x coordinate of the chunk in world coordinates</param>
    /// <param name="PosY">The y coordinate of the chunk in world coordinates</param>
    /// <param name="PosZ">The z coordinate of the chunk in world coordinates</param>
    public Chunk(int PosX, int PosY, int PosZ)
    {
        voxelTerrain = VoxelTerrain.Instance;
        worldX = PosX;
        worldY = PosY;
        worldZ = PosZ;

        chunkData = new byte[VoxelTerrain.CHUNK_VOLUMNE]; // 4096 blocks, 1 byte each

        m_ID = ID++;

        Vertices = new List<Vector3>();
        Triangles = new List<int>();
        UV = new List<Vector2>();

        IsClean = true;
        IsBuilded = false;
    }

    /// <summary>
    /// Updates mesh / mesh collider and resets the face count
    /// </summary>
    public void UpdateMesh()
    {
        VoxelTerrain.UpdateMeshCounter++;

        // Check if chunk is empty
        if (Vertices.Count == 0)
        {
            Object.Destroy(gameObject); // Destroy the gameObject and return
            Object.Destroy(mesh);

            Vertices.Clear();
            Triangles.Clear();
            UV.Clear();

            faceCount = 0;

            IsClean = false;
            return;
        }


        if (gameObject == null)
        {
            gameObject = Object.Instantiate(VoxelTerrain.ChunkPrefab_Static, worldPosition, Quaternion.identity) as GameObject;
            gameObject.name = string.Format("Chunk {0}", m_ID);
            //gameObject.transform.parent = voxelTerrain.gameObject.transform;  // Don't want to have my hierarchy being messed up with loads of chunks

            mesh = gameObject.GetComponent<MeshFilter>().sharedMesh;
            if (mesh == null)
                mesh = gameObject.GetComponent<MeshFilter>().sharedMesh = new Mesh();
            mesh.name = string.Format("ChunkMesh {0}", m_ID);
            meshCollider = gameObject.GetComponent<MeshCollider>();
        }

        mesh.Clear();
        mesh.vertices = Vertices.ToArray();
        mesh.triangles = Triangles.ToArray();
        mesh.uv = UV.ToArray();
        mesh.RecalculateNormals();

        meshCollider.sharedMesh = null;
        meshCollider.sharedMesh = mesh;

        Vertices.Clear();
        Triangles.Clear();
        UV.Clear();

        faceCount = 0;

        IsClean = false;
        NeedRepaint = false;
    }

    /// <summary>
    /// Creates a cube with all six sides
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The y-coordinate in local space</param>
    /// <param name="z">The z-coordinate in local space</param>
    /// <param name="blockID">The TextureID of the block</param>
    private void CreateCube(int x, int y, int z, byte blockID)
    {
        FaceTop(x, y, z, blockID);
        FaceBottom(x, y, z, blockID);
        FaceNorth(x, y, z, blockID);
        FaceSouth(x, y, z, blockID);
        FaceWest(x, y, z, blockID);
        FaceEast(x, y, z, blockID);
    }

    public void GenerateData()
    {
        VoxelTerrain.GenerateDataCounter++;
        float stone, grass; // Stores data for stone height and grass height

        for (int x = 0; x < VoxelTerrain.CHUNK_SIZE; x++)
            for (int z = 0; z < VoxelTerrain.CHUNK_SIZE; z++)
            {
                //stone = PerlinNoise.PerlinNoise_2D(worldX + x, worldZ + z, 50, 20, 1.0f) + 64;
                //grass = PerlinNoise.PerlinNoise_2D(worldX + x + 10000, worldZ + z + 10000, 100, 5, 1) + 1;

                for (int y = worldY; y < worldY + VoxelTerrain.CHUNK_SIZE; y++)
                {
                    if (y < 64 - 30)
                    {
                        stone = int.MaxValue;
                    }
                    else
                    {
                        stone = PerlinNoise.PerlinNoise_3D(worldX + x, y, worldZ + z, 30, 30, 1.0f) + 64;
                    }

                    SetBlockAt(x, y - worldY, z, (stone - y > 0.0f) ? Block.Stone : Block.Air);

                    // World boundries is bedrock
                    //if (y == 0)
                    //    SetBlockAt(x, y, z, Block.Bedrock);
                    //else
                    //{
                    //    if (y <= stone)
                    //    {
                    //        if (PerlinNoise.Noise_3D(x, y, z) >= 0.995f)
                    //            SetBlockAt(x, y - worldY, z, Block.IronOre);
                    //        else
                    //            SetBlockAt(x, y - worldY, z, Block.Stone);
                    //    }
                    //    else if (y <= stone + grass)
                    //        SetBlockAt(x, y - worldY, z, Block.Grass);
                    //    else
                    //        SetBlockAt(x, y - worldY, z, Block.Air);
                    //}
                }
            }

        IsBuilded = true;
        //GenerateMesh();
    }

    #region Face Creation
    /// <summary>
    /// Creates the top face of a cube
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The x-coordinate in local space</param>
    /// <param name="z">The x-coordinate in local space</param>
    /// <param name="blockID">The TextureID of the block</param>
    private void FaceTop(int x, int y, int z, byte blockID)
    {
        Vertices.Add(new Vector3(x + 0, y + 0, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 1, y + 0, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 0, y + 0, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 1, y + 0, z + 1) + Offset);

        if (blockID == Block.Grass)
        {
            Face(TextureManager.GetUV(TextureType.GrassTop));
            return;
        }
        else
        {
            Face(TextureManager.GetUV(blockID));
            return;
        }
    }
    /// <summary>
    /// Creates the bottom face of a cube
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The x-coordinate in local space</param>
    /// <param name="z">The x-coordinate in local space</param>
    /// <param name="blockID">The TextureID of the block</param>
    private void FaceBottom(int x, int y, int z, byte blockID)
    {
        Vertices.Add(new Vector3(x + 0, y - 1, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 1, y - 1, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 0, y - 1, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 1, y - 1, z + 0) + Offset);

        if (blockID == Block.Grass)
        {
            Face(TextureManager.GetUV(TextureType.Dirt));
            return;
        }
        else
        {
            Face(TextureManager.GetUV(blockID));
            return;
        }
    }

    /// <summary>
    /// Creates the east face of a cube
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The x-coordinate in local space</param>
    /// <param name="z">The x-coordinate in local space</param>
    /// <param name="blockID">The TextureID of the block</param>
    private void FaceEast(int x, int y, int z, byte blockID)
    {
        Vertices.Add(new Vector3(x + 1, y - 1, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 1, y - 1, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 1, y + 0, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 1, y + 0, z + 1) + Offset);

        Face(TextureManager.GetUV(blockID));
    }

    /// <summary>
    /// Creates the west face of a cube
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The x-coordinate in local space</param>
    /// <param name="z">The x-coordinate in local space</param>
    /// <param name="blockID">The TextureID of the block</param>
    private void FaceWest(int x, int y, int z, byte blockID)
    {
        Vertices.Add(new Vector3(x + 0, y - 1, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 0, y - 1, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 0, y + 0, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 0, y + 0, z + 0) + Offset);

        Face(TextureManager.GetUV(blockID));
    }

    /// <summary>
    /// Creates the north face of a cube
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The x-coordinate in local space</param>
    /// <param name="z">The x-coordinate in local space</param>
    /// <param name="blockID">The TextureID of the block</param>
    private void FaceNorth(int x, int y, int z, byte blockID)
    {
        Vertices.Add(new Vector3(x + 1, y - 1, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 0, y - 1, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 1, y + 0, z + 1) + Offset);
        Vertices.Add(new Vector3(x + 0, y + 0, z + 1) + Offset);

        Face(TextureManager.GetUV(blockID));
    }

    /// <summary>
    /// Creates the south face of a cube
    /// </summary>
    /// <param name="x">The x-coordinate in local space</param>
    /// <param name="y">The x-coordinate in local space</param>
    /// <param name="z">The x-coordinate in local space</param>
    /// <param name="texture">The TextureID of the block</param>
    private void FaceSouth(int x, int y, int z, byte blockID)
    {
        Vertices.Add(new Vector3(x + 0, y - 1, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 1, y - 1, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 0, y + 0, z + 0) + Offset);
        Vertices.Add(new Vector3(x + 1, y + 0, z + 0) + Offset);

        Face(TextureManager.GetUV(blockID));
    }

    /// <summary>
    /// Adds a new face to the mesh
    /// </summary>
    /// <param name="texturePos">The upper left corner on the atlas in int</param>
    private void Face(Vector2 texturePos)
    {
        int faceCountTimes4 = faceCount * 4;
        Triangles.Add(faceCountTimes4 + 0);
        Triangles.Add(faceCountTimes4 + 2);
        Triangles.Add(faceCountTimes4 + 3);
        Triangles.Add(faceCountTimes4 + 0);
        Triangles.Add(faceCountTimes4 + 3);
        Triangles.Add(faceCountTimes4 + 1);

        float x = texturePos.x;
        float y = texturePos.y;

        UV.Add(new Vector2(x + 0, y + 0) * TextureManager.TEX_FACTOR);
        UV.Add(new Vector2(x + 1, y + 0) * TextureManager.TEX_FACTOR);
        UV.Add(new Vector2(x + 0, y + 1) * TextureManager.TEX_FACTOR);
        UV.Add(new Vector2(x + 1, y + 1) * TextureManager.TEX_FACTOR);

        ++faceCount;
    }
    #endregion

    /// <summary>
    /// Creates the voxel mesh for the data of the chunk
    /// </summary>
    public void GenerateMesh()
    {
        byte blockID = Block.Air;
        for (int x = 0; x < VoxelTerrain.CHUNK_SIZE; ++x)
            for (int y = 0; y < VoxelTerrain.CHUNK_SIZE; ++y)
                for (int z = 0; z < VoxelTerrain.CHUNK_SIZE; ++z)
                {
                    blockID = GetBlockAt(x, y, z);
                    // Do not create air blocks
                    if (blockID == Block.Air)
                        continue;

                    // Above
                    if (GetBlockAt(x, y + 1, z) == Block.Air)
                        FaceTop(x, y, z, blockID);

                    // Below
                    if (GetBlockAt(x, y - 1, z) == Block.Air)
                        FaceBottom(x, y, z, blockID);

                    // East
                    if (GetBlockAt(x + 1, y, z) == Block.Air)
                        FaceEast(x, y, z, blockID);

                    // West
                    if (GetBlockAt(x - 1, y, z) == Block.Air)
                        FaceWest(x, y, z, blockID);

                    // North
                    if (GetBlockAt(x, y, z + 1) == Block.Air)
                        FaceNorth(x, y, z, blockID);

                    // South
                    if (GetBlockAt(x, y, z - 1) == Block.Air)
                        FaceSouth(x, y, z, blockID);
                }

        VoxelTerrain.GenerateMeshCounter++;
        //UpdateMesh();
    }

    /// <summary>
    /// Returns the TextureID of a block in local space
    /// </summary>
    /// <param name="x">The x coordinate in local space</param>
    /// <param name="y">The y coordinate in local space</param>
    /// <param name="z">The z coordinate in local space</param>
    public byte GetBlockAt(int x, int y, int z)
    {
        if (x >= VoxelTerrain.CHUNK_SIZE || x < 0 || y >= VoxelTerrain.CHUNK_SIZE || y < 0 || z >= VoxelTerrain.CHUNK_SIZE || z < 0)
        {
            return voxelTerrain.GetBlockAt(x + worldX, y + worldY, z + worldZ);
        }
        return chunkData[x + (y * VoxelTerrain.CHUNK_SIZE) + (z * VoxelTerrain.CHUNK_SIZE * VoxelTerrain.CHUNK_SIZE)];
    }
    /// <summary>
    /// Returns the TextureID of a block in local space
    /// </summary>
    /// <param name="pos">The position in local space</param>
    /// <returns></returns>
    public byte GetBlockAt(Point3 pos)
    {
        return GetBlockAt(pos.x, pos.y, pos.z);
    }

    /// <summary>
    /// Sets the TextureID of a block in local space
    /// </summary>
    /// <param name="x">The x coordinate in local space</param>
    /// <param name="y">The y coordinate in local space</param>
    /// <param name="z">The z coordinate in local space</param>
    /// <param name="blockID">The new TextureID</param>
    public void SetBlockAt(int x, int y, int z, byte blockID)
    {
        if (x >= VoxelTerrain.CHUNK_SIZE || x < 0 || y >= VoxelTerrain.CHUNK_SIZE || y < 0 || z >= VoxelTerrain.CHUNK_SIZE || z < 0)
            return;

        chunkData[x + (y * VoxelTerrain.CHUNK_SIZE) + (z * VoxelTerrain.CHUNK_SIZE * VoxelTerrain.CHUNK_SIZE)] = blockID;
    }
    /// <summary>
    /// Sets the TextureID of a block in local space
    /// </summary>
    /// <param name="pos">The position in local space</param>
    /// <param name="textureID">The new TextureID</param>
    public void SetBlockAt(Point3 pos, byte textureID)
    {
        SetBlockAt(pos.x, pos.y, pos.z, textureID);
    }

    /// <summary>
    /// Cleans up the unity member of the class
    /// </summary>
    public void CleanUp()
    {
        Object.DestroyImmediate(gameObject);
        Object.DestroyImmediate(mesh);
        IsClean = true;
    }

    public void Repaint()
    {
        if (NeedRepaint == false)
        {
            NeedRepaint = true;
            voxelTerrain.RepaintList.Add(this);
        }
    }

    public Chunk GetLeftChunk()
    {
        return voxelTerrain.GetChunkAt(worldPosition + new Point3(-VoxelTerrain.CHUNK_SIZE, 0, 0));
    }
    public Chunk GetRightChunk()
    {
        return voxelTerrain.GetChunkAt(worldPosition + new Point3(VoxelTerrain.CHUNK_SIZE, 0, 0));
    }
    public Chunk GetFrontChunk()
    {
        return voxelTerrain.GetChunkAt(worldPosition + new Point3(0, 0, VoxelTerrain.CHUNK_SIZE));
    }
    public Chunk GetBackChunk()
    {
        return voxelTerrain.GetChunkAt(worldPosition + new Point3(0, 0, -VoxelTerrain.CHUNK_SIZE));
    }
    public Chunk GetTopChunk()
    {
        return voxelTerrain.GetChunkAt(worldPosition + new Point3(0, VoxelTerrain.CHUNK_SIZE, 0));
    }
    public Chunk GetBottomChunk()
    {
        return voxelTerrain.GetChunkAt(worldPosition + new Point3(0, -VoxelTerrain.CHUNK_SIZE, 0));
    }

    public string ChunkDataString()
    {
        string result = "";

        foreach (byte blockID in chunkData)
        {
            result += blockID + " ";
        }

        return result;
    }

    public override int GetHashCode()
    {
        unchecked // Overflow is fine, just wrap
        {
            int hash = 13;
            hash = hash * 3 + worldX.GetHashCode();
            hash = hash * 3 + worldY.GetHashCode();
            hash = hash * 3 + worldZ.GetHashCode();
            return hash;
        }
    }

    public override bool Equals(object obj)
    {
        Chunk c = obj as Chunk;

        if (c == null)
            return false;

        return worldX == c.worldX && worldY == c.worldY && worldZ == c.worldZ;
    }
}
